<?php
/**
 *	KLF Framework
 *
 *	@author Kristian Oye <kristianoye@gmail.com> 
 *	@copyright (C) 2010-2011, Kristian Oye
 */
namespace KLF\System\Web\Routing
{
	use \KLF\System\KComponent;
	use \KLF\System\Web\KHttpContext;
	use \KLF\System\Errors\KArgumentException;
	
	/**
	 *  Class KRoute
	 *
	 *  @package KLF\System\Web\Routing
	 *  @access public
	 *  @author Kristian Oye <kristianoye@gmail.com>
	 *  @since 0.1
	 *  @version 0.1
	 *  
	 *  @method KRoute NewInstance
	 *  
	 *  @property KRouteDictionary $Constraints The constraints on the route.
	 *  @property KRouteDictionary $DataTokens The data tokens.
	 *  @property KRouteDictionary $Defaults The default values for the route.
	 *  @property bool $CaseSensitive Is true if the route is case sensitive.
	 *  @property string $Name The name of the route.
	 *  @property array $Namespaces
	 *  @property string $RouteDelimiter The character used for delimiting routes.
	 *  @property IRouteHandler $RouteHandler The handler for the route.
	 *  @property string $RouteRegex The regex used for matching the route.
	 *  @property string $Url The route URL.
	 */
	class KRoute extends KComponent
	{
		/**
		 * A collection of rules controlling whether the route matched a request.
		 * 
		 * @var KRouteDictionary $_propConstraints
		 */
		protected $_propConstraints;
		
		/**
		 * The DataTokens collection provides a means of passing additional information not
		 * supplied via the request URL.
		 * 
		 * @var KRouteDictionary $_propDataTokens
		 */
		protected $_propDataTokens;
		
		/**
		 * Provides defaults for parameters in the route.
		 * 
		 * @var KRouteDictionary $_propDefaults
		 */
		protected $_propDefaults;
		
		/**
		 * Is the URL case-sensitive?
		 * 
		 * @var bool $_propCaseSensitive
		 */
		protected $_propCaseSensitive;
		
		/**
		 * The name of the route.
		 * 
		 * @var string $_propName
		 */
		protected $_propName;
		
		/**
		 * Set the namespaces to be used in the route.
		 * 
		 * @var array $_propNamespaces
		 */
		protected $_propNamespaces;
		
		/**
		 * The delimiter for splitting the request path with (Defaults to /)
		 * 
		 * @var string $_propRouteDelimiter
		 */
		protected $_propRouteDelimiter;
		
		/**
		* The IRouteHandler object that will handle the request.
		*
		* @var IRouteHandler $_propRouteHandler
		*/
		protected $_propRouteHandler;
		
		/**
		 * The regular expression used for matching against request URLs.
		 * 
		 * @var string $_propRouteRegex
		 */
		protected $_propRouteRegex;
		
		/**
		 * The URL to use when attempting to match requests.
		 * 
		 * @var string
		 */
		protected $_propUrl;
		
		/**
		 * Constructs a new route object.
		 * 
		 * @param array $args The arguments required to create the route.
		 */
		public function __construct($args = array())
		{
			if(!is_array($args))
			{
				throw new KArgumentException("Arg 1 must be array");
			}

			$this->_propCaseSensitive = true;
			$this->_propRouteDelimiter = '/';
			
			$this->_propDefaults = new KRouteDictionary();
			$this->_propConstraints = new KRouteDictionary();
			$this->_propDataTokens = new KRouteDictionary();
			$this->_propNamespaces = array();
			
			foreach($args as $k => $v)
			{
				switch(strtolower($k))
				{
					case 'casesensitive':	$this->_propCaseSensitive = $v; break;
					case 'constraints':		$this->_propConstraints->FromArray($v); break;
					case 'datatokens':		$this->_propDataTokens->FromArray($v); break;
					case 'defaults':		$this->_propDefaults->FromArray($v); break;
					case 'name':			$this->_propName = $v; break;
					case 'url':				$this->_propUrl = $v; break;
					case 'routehandler':	$this->_propRouteHandler = $v; break;
					case 'routedelimiter':	$this->_propRouteDelimiter = $v; break;
				}
			}
		}
		
		/**
		 * Gets a KRouteData object
		 *
		 * @author Kristian Oye <kristianoye@gmail.com>
		 * @since 0.1
		 * 
		 * @param $context KHttpContext
		 * @return \KLF\System\Web\Routing\KRouteData 
		*/
		public function GetRouteData(KHttpContext $context)
		{
			$regex = $this->GetRouteExpression();
			
			// Check to see if the route matched the incoming request
			// TODO: Add support for query-based routing.
			if (preg_match($regex, $context->Request->LocalPath, $matches))
			{
				//  Populate missing values with their default values.
				foreach($this->_propDefaults as $key => $val)
				{
					if (!isset($matches[$key]))
					{
						$matches[$key] = $val;
					}
				}
				
				if(!$this->ProcessConstraints($context, $matches, KRouteDirection::IncomingRequest()))
				{
					return false;
				}
				
				if (!isset($this->_propRouteHandler))
				{
					$routeData = new KRouteData($this, ($this->_propRouteHandler = new KMvcRouteHandler()));
				}
				else
				{
					$routeData = new KRouteData($this, new $this->_propRouteHandler);
				}
				
				foreach($matches as $key => $val)
				{
					if (is_string($key))
					{
						$routeData->Values->Add($key, $val);
					}
					if (is_int($key) && $key > 0)
					{
						$routeData->ValuesByPosition->Add($val);
					}
				}
				if(isset($this->_propDataTokens))
				{
					foreach($this->_propDataTokens as $key => $val)
					{
						$routeData->DataTokens->Add($key, $val);
					}
				}
				return $routeData;
			}
			
			// It did not match
			return false;
		}
		
		/**
		 * Returns the route as a regular expression that can be used to 
		 * match against requests.
		 *
		 * @author Kristian Oye <kristianoye@gmail.com>
		 * @since 0.1
		 * @return RegularExpression The expression to try and match against.
		 * @throws KArgumentException
		*/
		public function GetRouteExpression()
		{
			if(!isset($this->_propRouteRegex))
			{
				$escapedDelimiter = preg_replace("#([\\/\\.]{1})#", '\\\${1}', $this->_propRouteDelimiter);
				$matches = array_filter(preg_split('/('.$escapedDelimiter.')/', 
					$this->_propUrl, -1, PREG_SPLIT_DELIM_CAPTURE));
				$count = count($matches);
				$defaults = $lastDelimiter = false;
				
				for($i=0; $i<=$count; $i++)
				{
					if(!isset($matches[$i]))
						continue;
						
					$token = $matches[$i] = str_replace('/', '\/', $matches[$i]);
					
					if (strpos($token, '{{') !== false)
					{
						while(($x = strpos($token, '{{')) !== false)
						{
							if(($y = strpos($token, '}}')) === false)
							{
								throw new KArgumentException("Unterminated token in route ($token)");
							}
							$tokenName = substr($token, $x+2, ($y - $x - 2));
							
							if (($n = strpos($tokenName, ':')) !== false)
							{
								$regex = substr($tokenName, $n+1);
								$tokenName = substr($tokenName,0,$n);
							}
							else if($tokenName[0] === '*')
							{
								if($i < ($count-1))
								{
									throw new KArgumentException("$token must be the last token expression or * prefixed removed.");
								}
								$tokenName = substr($tokenName, 1);
								$regex = '.+';
							}
							else
							{
								$regex = '[^'.$escapedDelimiter.']+';
							}
							
							if(isset($this->Defaults[$tokenName]))
							{
								$defaults = $tokenName;
								if($lastDelimiter !== false)
									$matches[$lastDelimiter] = '['.$escapedDelimiter.']{0,1}';
								$regex = '(?<' . $tokenName . '>'.$regex.'){0,1}';
							}
							else
							{
								if($defaults !== false)
								{
									throw new KArgumentException('All parameters after {{'.$defaults.'}} must have a default value.');
								}
								$regex = '(?<' . $tokenName . '>'.$regex.'){1}';
							}
							$token = substr($token, 0, $x).$regex.substr($token, $y+2);
						}
						$matches[$i] = $token;
					}
					else if($token === $escapedDelimiter)
					{
						$matches[($lastDelimiter = $i)] = $defaults !== false ?
							$matches[$i] = '[\\'.$this->_propRouteDelimiter.']{0,1}' :
							$matches[$i] = '[\\'.$this->_propRouteDelimiter.']{1}';
					}
				}
				$matches[] = '['.$escapedDelimiter.']{0,1}';
				$this->RouteRegex = "#\n\t" . implode("\n\t\t", $matches) . '#x'
					. ($this->_propCaseSensitive ? '' : 'i');
			}
			return $this->_propRouteRegex;
		}
		
		/**
		 * Process constraints placed on the route.  Return true if constraints are met or false if not.
		 *
		 * @author Kristian Oye <kristianoye@gmail.com>
		 * @since 0.1
		 * @return bool True if all constraints are met, false if not
		*/
		protected function ProcessConstraints(KHttpContext $context, $matches, KRouteDirection $direction)
		{
			foreach($this->_propConstraints as $key => $val)
			{
				$constraints = is_array($val) ? $val : array($val);
					
				foreach($constraints as $c)
				{
					//  Regex constraint
					if (is_string($c))
					{
						if (isset($matches[$val]))
						{
							if (!preg_match($c, $matches[$val]))
							{
								return false;
							}
						}
					}
					else if(is_a($c, Constraints\KRouteConstraint::GetType()))
					{
						if (!$c->match($context, $this, $key, $matches, $direction))
						{
							return false;
						}
					}
					else
					{
						throw new KArgumentException(
							(is_object($val) ? get_class($val) : $val).
							" is neither a regex or valid constraint");
					}
				}
			}
			return true;
		}
	}
}
